]>
vault307.fbx.one Git - Sensory_Wall.git/blob - circuitPython/audio_spectrum_lightshow/CircuitPython 8.x/code.py
1 # SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
3 # SPDX-License-Identifier: MIT
6 AUDIO SPECTRUM LIGHT SHOW for Adafruit EyeLights (LED Glasses + Driver).
7 Uses onboard microphone and a lot of math to react to music.
10 from array
import array
12 from time
import monotonic
13 from supervisor
import reload
15 from audiobusio
import PDMIn
17 import adafruit_is31fl3741
18 from adafruit_is31fl3741
.adafruit_rgbmatrixqt
import Adafruit_RGBMatrixQT
19 from rainbowio
import colorwheel
20 from ulab
import numpy
as np
21 # if using CP7 and below:
22 from ulab
.scipy
.signal
import spectrogram
23 # if using CP8 and above:
24 # from ulab.utils import spectrogram
27 # FFT/SPECTRUM CONFIG ----
29 fft_size
= 256 # Sample size for Fourier transform, MUST be power of two
30 spectrum_size
= fft_size
// 2 # Output spectrum is 1/2 of FFT result
31 # Bottom of spectrum tends to be noisy, while top often exceeds musical
32 # range and is just harmonics, so clip both ends off:
33 low_bin
= 10 # Lowest bin of spectrum that contributes to graph
34 high_bin
= 75 # Highest bin "
37 # HARDWARE SETUP ---------
39 # Manually declare I2C (not board.I2C() directly) to access 1 MHz speed...
40 i2c
= I2C(board
.SCL
, board
.SDA
, frequency
=1000000)
42 # Initialize the IS31 LED driver, buffered for smoother animation
43 #glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
44 glasses
= Adafruit_RGBMatrixQT(i2c
, allocate
=adafruit_is31fl3741
.MUST_BUFFER
)
46 glasses
.show() # Clear any residue on startup
47 glasses
.set_led_scaling(0xFF)
48 glasses
.global_current
= 5 # Not too bright please
51 # Initialize mic and allocate recording buffer (default rate is 16 MHz)
52 mic
= PDMIn(board
.MICROPHONE_CLOCK
, board
.MICROPHONE_DATA
, bit_depth
=16)
53 rec_buf
= array("H", [0] * fft_size
) # 16-bit audio samples
56 # FFT/SPECTRUM SETUP -----
58 # To keep the display lively, tables are precomputed where each column of
59 # the matrix (of which there are few) is the sum value and weighting of
60 # several bins from the FFT spectrum output (of which there are many).
61 # The tables also help visually linearize the output so octaves are evenly
62 # spaced, as on a piano keyboard, whereas the source spectrum data is
63 # spaced by frequency in Hz.
66 spectrum_bits
= log(spectrum_size
, 2) # e.g. 7 for 128-bin spectrum
67 # Scale low_bin and high_bin to 0.0 to 1.0 equivalent range in spectrum
68 low_frac
= log(low_bin
, 2) / spectrum_bits
69 frac_range
= log(high_bin
, 2) / spectrum_bits
- low_frac
71 for column
in range(glasses
.width
):
72 # Determine the lower and upper frequency range for this column, as
73 # fractions within the scaled 0.0 to 1.0 spectrum range. 0.95 below
74 # creates slight frequency overlap between columns, looks nicer.
75 lower
= low_frac
+ frac_range
* (column
/ glasses
.width
* 0.95)
76 upper
= low_frac
+ frac_range
* ((column
+ 1) / glasses
.width
)
77 mid
= (lower
+ upper
) * 0.5 # Center of lower-to-upper range
78 half_width
= (upper
- lower
) * 0.5 # 1/2 of lower-to-upper range
79 # Map fractions back to spectrum bin indices that contribute to column
80 first_bin
= int(2 ** (spectrum_bits
* lower
) + 1e-4)
81 last_bin
= int(2 ** (spectrum_bits
* upper
) + 1e-4)
82 bin_weights
= [] # Each spectrum bin's weighting will be added here
83 for bin_index
in range(first_bin
, last_bin
+ 1):
84 # Find distance from column's overall center to individual bin's
85 # center, expressed as 0.0 (bin at center) to 1.0 (bin at limit of
86 # lower-to-upper range).
87 bin_center
= log(bin_index
+ 0.5, 2) / spectrum_bits
88 dist
= abs(bin_center
- mid
) / half_width
89 if dist
< 1.0: # Filter out a few math stragglers at either end
90 # Bin weights have a cubic falloff curve within range:
91 dist
= 1.0 - dist
# Invert dist so 1.0 is at center
92 bin_weights
.append(((3.0 - (dist
* 2.0)) * dist
) * dist
)
93 # Scale bin weights so total is 1.0 for each column, but then mute
94 # lower columns slightly and boost higher columns. It graphs better.
95 total
= sum(bin_weights
)
97 (weight
/ total
) * (0.8 + idx
/ glasses
.width
* 1.4)
98 for idx
, weight
in enumerate(bin_weights
)
100 # List w/five elements is stored for each column:
101 # 0: Index of the first spectrum bin that impacts this column.
102 # 1: A list of bin weights, starting from index above, length varies.
103 # 2: Color for drawing this column on the LED matrix. The 225 is on
104 # purpose, providing hues from red to purple, leaving out magenta.
105 # 3: Current height of the 'falling dot', updated each frame
106 # 4: Current velocity of the 'falling dot', updated each frame
111 colorwheel(225 * column
/ glasses
.width
),
116 # print(column_table)
119 # MAIN LOOP -------------
121 dynamic_level
= 10 # For responding to changing volume levels
122 frames
, start_time
= 0, monotonic() # For frames-per-second calc
125 # The try/except here is because VERY INFREQUENTLY the I2C bus will
126 # encounter an error when accessing the LED driver, whether from bumping
127 # around the wires or sometimes an I2C device just gets wedged. To more
128 # robustly handle the latter, the code will restart if that happens.
130 mic
.record(rec_buf
, fft_size
) # Record batch of 16-bit samples
131 samples
= np
.array(rec_buf
) # Convert to ndarray
132 # Compute spectrogram and trim results. Only the left half is
133 # normally needed (right half is mirrored), but we trim further as
134 # only the low_bin to high_bin elements are interesting to graph.
135 spectrum
= spectrogram(samples
)[low_bin
: high_bin
+ 1]
136 # Linearize spectrum output. spectrogram() is always nonnegative,
137 # but add a tiny value to change any zeros to nonzero numbers
138 # (avoids rare 'inf' error)
139 spectrum
= np
.log(spectrum
+ 1e-7)
140 # Determine minimum & maximum across all spectrum bins, with limits
141 lower
= max(np
.min(spectrum
), 4)
142 upper
= min(max(np
.max(spectrum
), lower
+ 6), 20)
144 # Adjust dynamic level to current spectrum output, keeps the graph
145 # 'lively' as ambient volume changes. Sparkle but don't saturate.
146 if upper
> dynamic_level
:
147 # Got louder. Move level up quickly but allow initial "bump."
148 dynamic_level
= upper
* 0.7 + dynamic_level
* 0.3
150 # Got quieter. Ease level down, else too many bumps.
151 dynamic_level
= dynamic_level
* 0.5 + lower
* 0.5
153 # Apply vertical scale to spectrum data. Results may exceed
154 # matrix height...that's OK, adds impact!
155 #data = (spectrum - lower) * (7 / (dynamic_level - lower))
156 data
= (spectrum
- lower
) * ((glasses
.height
+ 2) / (dynamic_level
- lower
))
158 for column
, element
in enumerate(column_table
):
159 # Start BELOW matrix and accumulate bin weights UP, saves math
160 first_bin
= element
[0]
161 column_top
= glasses
.height
+ 1
162 for bin_offset
, weight
in enumerate(element
[1]):
163 column_top
-= data
[first_bin
+ bin_offset
] * weight
165 if column_top
< element
[3]: # Above current falling dot?
166 element
[3] = column_top
- 0.5 # Move dot up
167 element
[4] = 0 # and clear out velocity
169 element
[3] += element
[4] # Move dot down
170 element
[4] += 0.2 # and accelerate
172 column_top
= int(column_top
) # Quantize to pixel space
173 for row
in range(column_top
): # Erase area above column
174 glasses
.pixel(column
, row
, 0)
175 for row
in range(column_top
, glasses
.height
): # Draw column
176 glasses
.pixel(column
, row
, element
[2])
177 glasses
.pixel(column
, int(element
[3]), 0xE08080) # Draw peak dot
179 glasses
.show() # Buffered mode MUST use show() to refresh matrix
182 # print(frames / (monotonic() - start_time), "FPS")
184 except OSError: # See "try" notes above regarding rare I2C errors.